package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

public class Server {
    /*
        java.net.ServerSocket 运行在服务端的ServerSocket主要有两个作用
        1:打开服务端口，客户端就是通过这个端口与服务端建立连接的。本案例打开的就是8088端口
        2:监听服务端口(8088),一旦一个客户端通过该端口与服务端请求建立连接，此时会立即返回
          一个Socket实例，服务端就通过该实例与该客户端交互。

        将ServerSocket比喻为"总机"。客户端的"电话"都是先拨通到"总机"上，然后"总机"分配一台"电话"，
        通过这个"电话"就可以和该客户端沟通了
     */
    private ServerSocket serverSocket;
    //private PrintWriter[] allout = {};
    private Collection<PrintWriter> allout = new ArrayList<>();

    public Server() {
        try {
            System.out.println("正在启动服务器");
                /*
                这里实例化ServerSocket的同时我们要指定服务端口，客户端就是通过该端口连接的
                注意:这个端口不能与操作系统上其他应用程序已经打开的端口重复，否则会抛出异常:
                java.net.BindException:address already in use
                         绑定异常       地址     已经     被使用
                解决办法:更换端口，直到可用。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务器启动完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端link  ");
                 /*
                ServerSocket提供的一个重要方法:
                Socket accept()
                该方法用于接收客户端的连接，该方法是一个阻塞方法，调用后程序会"卡住"，直到一个
                客户端连接了ServerSocket,此时该方法会立即返回一个Socket实例。服务端通过该
                实例就可以与连接的客户端交互了。
                该方法理解为就是总机的"接电话"动作。
             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户连接上了");
                /*
                Socket一个重要方法:
                InputStream getInputStream()
                通过该方法获取的字节输入流可以读取来自远端计算机发送过来的数据
             */
                //启动一个线程负责与该客户端交互
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread t = new Thread(clientHandler);
                t.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    private class ClientHandler implements Runnable {
        private Socket socket;
        private String host;

        public ClientHandler(Socket socket) {
            this.socket = socket;//记录该客户端的IP地址信息
            //通过socket获取远端计算机的地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            PrintWriter pw = null;
            try {
/*
                Socket一个重要方法:
                InputStream getInputStream()
                通过该方法获取的字节输入流可以读取来自远端计算机发送过来的数据*/
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //通过socket获取输出流，用于将消息发送给该客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                //将输出流存入到共享数组中
                /*
                    这里指定this作为同步监视器对象不可以
                    原因在于不同的线程都有自己的线程任务(ClientHandler实例)
                    而每个ClientHandler的run方法中的this就是该实例,因此多个
                    线程看到的不是同一个对象!
                 */
//                synchronized (this) {
                /*
                    通常情况下当多个线程并发访问同一个临界资源时,我们就将该临界
                    资源作为同步监视器对象即可!
                    但是,这里allOut虽然是临界资源(多个线程都在对他扩容添元素)
                    但是这里不能用它,因为扩容会创建新的数组对象,这意味着allOut
                    指向的数组对象会一直改变,导致其它竞争的线程看到的不再是同一个
                    锁对象了!//                synchronized (allOut) {
                 */
                synchronized (allout) {
                    /*allout = Arrays.copyOf(allout, allout.length + 1);
                    allout[allout.length - 1] = pw;  //c.add(pw);*/
                    allout.add(pw);
                }
                sendMessage(host + "上线了，当前在线人数" + allout.size());
/*
                    如果客户端异常断开，服务端会抛出异常
                    java.net.SocketException: Connection reset
                    该异常无法避免，因为这个是客户端的操作导致的。
                 */
                String line;
                while ((line = br.readLine()) != null) {
                    sendMessage(host + "说：" + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
                //处理该客户端下线后的操作
                //1将该客户端的输出流从共享集合allOut中删除
            } finally {
                synchronized (allout) {
                    allout.remove(pw);
                }
            }
            sendMessage(host + "下线了，当前在线人数" + allout.size());
            //2将对应该客户端的socket关闭;
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //将消息广播给所有客户端
        private void sendMessage(String message) {
            System.out.println(message);
            //将消息发送给所有客户端
            synchronized (allout) {
                for (PrintWriter pw : allout) {
                    pw.println(message);
                }
            }
        }
    }
}